iT邦幫忙

2026 iThome 鐵人賽

DAY 16
1

配合分支:day16/start,Repo: 按我

Day 15 修正讀取照片錯誤,引入了 sealed class 來表示:成功、失敗。本章要來說明,有很多方式可以修正問題,有些時候,並不一定有 標準答案。(當然嚴重的資安、隱私⋯ 等,問題則另當別論)。昨天的案例可以來說明各種修正的方式,以及 為什麼要選擇 Sealed Class 來修正,這牽扯到每一個 class 的責任歸屬。

今天來新增一個情境:判斷照片「不存在」。(昨天是每一個錯誤,都丟一樣的訊息,對這個情境做擴充)。驗證方式是隨意將一個照片在 App 儲存空間改名(可以透過 Android Studio 的 Device Explorer 來辦到)

調整方向

大概分為幾個層面來做修正:

  1. 直接在 OwnedPhotoImage 碰到 Unavailable 時,順便檢查檔案是否存在?
  2. 新增一個 sealed class PhotoReadResult 代表「檔案不存在」

三者皆可以修正問題, 說明的方向會是 1 -> 2。先講結論,最後會選擇 2 來進行調整。待我一一說明如下。

直接在 OwnedPhotoImage 碰到 Unavailable 時,順便檢查檔案是否存在?

Unavailable就已經是錯誤,直接在這邊判斷就好囉。的確可以這麼做沒有錯,程式碼大概會長成這個樣子:

https://ithelp.ithome.com.tw/upload/images/20260904/20141615ks9bhZKmqo.png

遇到問題,去問 fileName 去問檔案是否存在,不存在就將顯示的訊息變成:
照片暫時無法讀取。

問題解決了,但哪裡怪怪的?

雖然放在這,的確修正了問題,回到本章要提的「權責」。OwnedPhotoImage 是一個 ComposeView (又或者,稱為 Composable 或 Composable View)。名稱裡頭有 View 就代表這個程式碼最好只負責「純顯示畫面」。

不過在這裡他做了其他的工作:去問系統檔案存不存在?這很顯然的已經超過了純顯示畫面的範疇。

context.filesDir.resolve(fileName).exists()

第二點是,在這裡多了一個 if 判斷式。如果未來有更多的問題需要被檢查,例如「這個照片副檔名支不支援?」例如使用者傳了一個 Raw 相片格式之類的?
除了判斷式越來越複雜,變得不好讀以外。(未來可能進化成 when 判斷式⋯ 🥤 )代表未來測試到顯示錯誤照片邏輯這一塊,又多了一個變數/路徑需要測試。
顯然未來這邊變數越加越多,走到 Unavailable 會越來越複雜,也多了壞掉的可能。

新增一個 sealed class 代表「檔案不存在」?

依然是一個比較好的處理方式。怎麼說?先來產生一個新的 PhotoReadResult 種類—— MissingFile

class MissingFile : PhotoReadResult()

雖然牽一髮而動全身,不過在「資料流」上會單純很多。同時,權責方便也會比較清晰,待我分解。

open() 時多了一個錯誤要處理:

https://ithelp.ithome.com.tw/upload/images/20260904/20141615NeypsqSjDr.png

從程式碼來看,剛好配合 inputStream 出現錯誤時,會拋出的 Exception。這裡回新增的 MissingFile 錯誤。
而這個 open() ,目前只有在 OwnedPhotoImage 這個「畫面」使用。

要擴充也十分的單純,只要 when 表達式補上即可。

is PhotoReadResult.MissingFile -> {
	Box(
		modifier = modifier.clipToBounds(),
		contentAlignment = Alignment.Center
	) {
		Text(text = "照片檔案已遺失")
	}
}

容易理解,以後要改也很簡單。

分析差異

為何這類的「讀取檔案」的問題,放在 LocalPhotoStore 比較好呢?這個 Class 的責任是去取得、寫入照片到 App 儲存空間裡面,取得照片本來就是他的職責

但是 OwnedPhotoImage 的職責是「呈現目前的狀態到畫面上」。單純由自己去判斷是否有照片,有點逾越了他的職權。(抽象的來看啦)

從資料流來看也會比較清晰,ComposeView 等「View(中文應該翻譯成:視圖,但大家都講 View 啦)」從其他人那邊取得狀態後畫上去。而 LocalPhotoStore 負責取資料,給資料。

理想上資料流:

https://ithelp.ithome.com.tw/upload/images/20260904/20141615NgUEAzzDNz.png

結語

不過這並非最理想的使用方式,讀者可能發現明明 OwnedPhotoImage只是一個 View 怎麼可以自己生一個 LocalPhotoStore?欸,終於發現了這個問題了嗎?這會造成上面資料流的圖片有點對不起來。這一點,請待之後的課程再依序幫各位介紹。

TL;DR

  • Sealed Class:今天透過新增 MissingFile 把「檔案不存在」獨立成專屬的錯誤結果。
  • 責任歸屬(權責):每個類別只專注做好自己該做之事的設計原則;今天用來釐清檢查檔案問題不該交由畫面端負責。
  • View(ComposeView / Composable):專門負責在螢幕上呈現畫面的元件;今天的 OwnedPhotoImage 應該只管畫出畫面,不該越權去拿資料。
  • 狀態(State):系統或元件在某一刻所處的資料情況;今天 View 的核心職責就是被動接收狀態並如實顯示在畫面上。
  • 資料流:資料在各個程式元件之間流動的方向;今天透過由 LocalPhotoStore 提供狀態給 View 顯示,讓資料傳遞單純好懂。
  • Exception(例外):程式在執行期間發生非預期狀況時拋出的錯誤物件;今天在讀取串流失敗拋出 Exception 時,將它轉換成明確的結果狀態。
  • 檔案存在檢查(exists):向作業系統查詢特定路徑檔案是否存在的操作;今天說明此類存取屬於資料層職責,超出畫面的範疇。
  • 測試路徑:程式因為條件分支而產生需要被驗證的執行路線;今天提到在畫面增加額外邏輯會增加測試路徑與程式出錯的機率。

上一篇
[Day15] 錯誤處理
系列文
我的第一個手作りAndroid App!快樂學習物件導向程式設計!16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
AndyAWD
iT邦新手 1 級 ‧ 2026-09-04 22:32:54

期待之後的課程解答

我要留言

立即登入留言